package bolt

import (
	"context"
	"fmt"
	"github.com/beego/beego/v2/core/logs"
	"github.com/neo4j/neo4j-go-driver/v5/neo4j"
	"go1/db"
	"log"
	"time"
)

type Crud[T any] struct {
	//Driver   neo4j.DriverWithContext
	//Ctx      context.Context
	//Database string
	db.CbCrud[T]
}

var Driver neo4j.DriverWithContext
var Ctx context.Context
var Database string

type CrudOpts struct {
	Tip        string
	Err        error
	Tx         neo4j.ManagedTransaction
	TxTimeout  int
	TxMetadata map[string]any
	Default    map[string]any
	Mm         int
}

const (
	NodeCreate          = 1
	PropertiesSet       = 2
	NodeDelete          = 3
	CreateRelationships = 4
	DeleteRelationships = 5
	WW                  = 6
)

var MemDb = Crud[any]{}

func (c *Crud[T]) Insert(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	opts.Mm = NodeCreate
	return c.GetOne(sql, params, opts)
}

func (c *Crud[T]) Info(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	return c.GetOne(sql, params, opts)
}

func (c *Crud[T]) Update(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	opts.Mm = PropertiesSet
	return c.GetOne(sql, params, opts)
}

func (c *Crud[T]) Delete(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	opts.Mm = NodeDelete
	return c.GetOne(sql, params, opts)
}

func (c *Crud[T]) DeleteRelation(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	opts.Mm = DeleteRelationships
	return c.GetOne(sql, params, opts)
}

func (c *Crud[T]) CreateRelation(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	opts.Mm = CreateRelationships
	return c.GetOne(sql, params, opts)
}

func (c *Crud[T]) Write(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	opts.Mm = WW
	return c.GetOne(sql, params, opts)
}

func (c *Crud[T]) Transaction(fun func(tx neo4j.ManagedTransaction) any, opts *CrudOpts) any {
	session := Driver.NewSession(Ctx, neo4j.SessionConfig{DatabaseName: Database})
	defer func(session neo4j.SessionWithContext, c1 context.Context) {
		err := session.Close(c1)
		if err != nil {
			logs.Error("Close Bolt Transaction Session Error, %v", err)
		}
	}(session, Ctx)
	sec := opts.TxTimeout
	if sec == 0 {
		sec = 5
	}
	reply, err := session.ExecuteWrite(Ctx, func(tx neo4j.ManagedTransaction) (any, error) {
		opts.Tx = tx
		if opts.Err != nil {
			return nil, opts.Err
		}
		return fun(tx), nil
	}, neo4j.WithTxTimeout(time.Duration(sec*1000000000)), neo4j.WithTxMetadata(opts.TxMetadata))
	if err != nil {
		logs.Error("Run Transaction Error, %v", err)
		panic(fmt.Errorf("run transaction error, %v", err))
	}
	return reply
}

func (c *Crud[T]) GetOne(sql string, params map[string]any, opts *CrudOpts) map[string]any {
	logs.Debug("run Bolt GetOne sql: %s, params: %v", sql, params)
	if opts.Tx != nil {
		run, err := opts.Tx.Run(Ctx, sql, params)
		if err != nil {
			logs.Error("run Bolt GetOne error, err: %v, sql: %s, params: %v", err, sql, params)
			panic(getTip(opts))
		}
		if !run.Next(Ctx) {
			return opts.Default
		}
		row := run.Record().AsMap()
		consume, err := run.Consume(Ctx)
		success(consume, opts)
		if err != nil {
			logs.Error("run Bolt GetOne Consume error, sql: %s, params: %v, error: %v", sql, params, err)
			panic("unexpected_error")
		}
		return row
	}
	query, err := neo4j.ExecuteQuery(Ctx, Driver, sql, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase(Database))
	if err != nil {
		logs.Error("run GetOne error: %v, sql: %s, params: %v", err, sql, params)
		panic(getTip(opts))
	}
	records := query.Records
	success(query.Summary, opts)
	if len(records) == 0 {
		return opts.Default
	}
	return records[0].AsMap()
}

func (c *Crud[T]) GetAll(sql string, params map[string]any, opts *CrudOpts) []map[string]any {
	logs.Debug("run GetAll sql: %s, params: %v", sql, params)
	if opts.Tx != nil {
		run, err := opts.Tx.Run(Ctx, sql, params)
		if err != nil {
			logs.Error("run GetAll error, sql: %s, params: %v", sql, params)
			panic(getTip(opts))
		}
		if !run.Next(Ctx) {
			return []map[string]any{}
		}
		records, err := run.Collect(Ctx)
		if err != nil {
			logs.Error("run GetAll Collect error, sql: %s, params: %v, error: %v", sql, params, err)
			panic(getTip(opts))
		}
		l := len(records)
		rows := make([]map[string]any, l)
		for i := 0; i < l; i++ {
			row := records[i].AsMap()
			rows[i] = row
		}
		return rows
	}
	query, err := neo4j.ExecuteQuery(Ctx, Driver, sql, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase(Database))
	if err != nil {
		logs.Error("run GetAll error, sql: %s, params: %v", sql, params)
		panic(getTip(opts))
	}
	success(query.Summary, opts)
	records := query.Records
	l := len(records)
	if l == 0 {
		return []map[string]any{}
	}
	rows := make([]map[string]any, l)
	for i := 0; i < l; i++ {
		row := records[i].AsMap()
		rows[i] = row
	}
	return rows
}

func BuildInsert(data map[string]any) string {
	sql := ""
	for key, _ := range data {
		if len(sql) > 0 {
			sql += ","
		}
		sql += key + ":$" + key
	}
	return sql
}

func BuildSet(obj string, data map[string]any) string {
	sql := ""
	for key, _ := range data {
		if len(sql) > 0 {
			sql += ","
		}
		sql += obj + "." + key + "=$" + key
	}
	return sql
}

func success(summary neo4j.ResultSummary, opts *CrudOpts) {
	if opts.Mm == 0 {
		return
	}
	var c bool
	var funName string
	co := summary.Counters()
	switch opts.Mm {
	case NodeCreate:
		c = co.NodesCreated() > 0
		funName = "NodeCreate"
	case PropertiesSet:
		c = co.ContainsUpdates()
		funName = "PropertiesSet"
	case NodeDelete:
		c = co.NodesDeleted() > 0
		log.Println(opts.Mm, co.NodesDeleted())
		funName = "NodeDelete"
	case CreateRelationships:
		c = co.RelationshipsCreated() > 0
		log.Println(opts.Mm, co.RelationshipsCreated())
		funName = "CREATE_RELATIONSHIPS"
	case DeleteRelationships:
		c = co.RelationshipsDeleted() > 0
		log.Println(opts.Mm, co.RelationshipsDeleted())
		funName = "DELETE_RELATIONSHIPS"
	case WW:
		c = co.ContainsUpdates()
		log.Println(opts.Mm, co.ContainsUpdates())
		funName = "WW"
	default:
		c = true
	}
	if !c {
		tip := opts.Tip
		if tip == "" {
			tip = "unexpected_error"
		}
		logs.Error("bolt %s error: %s, sql: %v, params: %v", funName, tip, summary.Query().Text(), summary.Query().Parameters())
		panic(fmt.Errorf(tip))
	}
}

func getTip(opts *CrudOpts) string {
	tip := opts.Tip
	if tip == "" {
		tip = "unexpected_error"
	}
	return tip
}
